/*
 * File    : TRTrackerServerProcessorUDP.java
 * Created : 20-Jan-2004
 * By      : parg
 * 
 * Azureus - a Java Bittorrent client
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details ( see the LICENSE file ).
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package connect.tracker.server;

/**
 * @author parg
 *
 */

import torrentlib.Constants;
import torrentlib.SystemTime;
import torrentlib.Debug;
import torrentlib.AEMonitor;
import torrentlib.util.logging.LogEvent;
import torrentlib.util.logging.Logger;
import torrentlib.util.logging.LogIDs;
import torrentlib.RandomUtils;
import torrentlib.HashWrapper;
import stdlib.security.types.SHA1Hasher;
import connect.tracker.server.TRTrackerServerPeerImpl;
import connect.tracker.server.TRTrackerServerTorrentImpl;
import connect.tracker.server.TRTrackerServerProcessor;
import connect.tracker.server.TRTrackerServerRequest;
import connect.tracker.server.TRTrackerServerPeer;
import connect.tracker.protocol.udp.PRUDPPacketReplyConnect;
import connect.tracker.protocol.udp.PRUDPPacketTracker;
import connect.tracker.protocol.udp.PRUDPPacketReplyAnnounce2;
import connect.tracker.protocol.udp.PRUDPTrackerCodecs;
import connect.tracker.protocol.udp.PRUDPPacketRequestScrape;
import connect.tracker.protocol.udp.PRUDPPacketReplyAnnounce;
import connect.tracker.protocol.udp.PRUDPPacketRequestAnnounce2;
import connect.tracker.protocol.udp.PRUDPPacketReplyScrape;
import connect.tracker.protocol.udp.PRUDPPacketReplyError;
import connect.tracker.protocol.udp.PRUDPPacketReplyScrape2;
import connect.tracker.protocol.udp.PRUDPPacketRequestAnnounce;
import connect.tracker.protocol.PRHelpers;
import java.net.*;
import java.io.*;
import java.security.SecureRandom;
import java.util.*;



import comm.udp.PRUDPPacket;
import comm.udp.PRUDPPacketRequest;

public class 
TRTrackerServerProcessorUDP
	extends		TRTrackerServerProcessor
{
	private static final LogIDs LOGID = LogIDs.TRACKER;
		// client may connect + then retry announce up to 4 times -> * 6
	
	public static final long CONNECTION_ID_LIFETIME	= PRUDPPacket.DEFAULT_UDP_TIMEOUT*6;
	
	private TRTrackerServerUDP		server;
	private DatagramSocket			socket;
	private DatagramPacket			request_dg;
	
	private static Map<Long,connectionData>				connection_id_map 	= new LinkedHashMap<Long,connectionData>();
	private static Map<String,List<connectionData>>		connection_ip_map 	= new HashMap<String,List<connectionData>>();
	private static long									last_timeout_check;
	
	private static SecureRandom		random				= RandomUtils.SECURE_RANDOM;
	private static AEMonitor		random_mon 			= new AEMonitor( "TRTrackerServerUDP:rand" );

	static{
	  	PRUDPTrackerCodecs.registerCodecs();
	}
	
	protected
	TRTrackerServerProcessorUDP(
		TRTrackerServerUDP		_server,
		DatagramSocket			_socket,
		DatagramPacket			_packet )
	{
		server			= _server;
		socket			= _socket;
		request_dg		= _packet;
	}
	
	public void
	runSupport()
	{				
		byte[]	input_buffer = new byte[request_dg.getLength()];
		
		System.arraycopy( request_dg.getData(), 0, input_buffer, 0, input_buffer.length );
		
		int	packet_data_length = input_buffer.length;
		
		String	auth_user			= null;
		byte[] 	auth_user_bytes		= null;
		byte[]	auth_hash			= null;
		
		
		if ( server.isTrackerPasswordEnabled()){
		
				// auth detail should be attached to the packet. Auth details are 16
				// bytes
			
			if ( input_buffer.length < 17 ){
				
				Logger.log(new LogEvent(LOGID, LogEvent.LT_WARNING,
						"TRTrackerServerProcessorUDP: "
								+ "packet received but authorisation missing")); 

				return;
			}
			
			packet_data_length -= 16;
			
			auth_user_bytes = new byte[8];
			
			auth_hash = new byte[8];
			
			System.arraycopy( input_buffer, packet_data_length, auth_user_bytes, 0, 8 );
			
			int	user_len = 0;
			
			while( user_len < 8 && auth_user_bytes[user_len] != 0 ){
				
				user_len++;
			}
			
			auth_user = new String( auth_user_bytes, 0, user_len );
			
			System.arraycopy( input_buffer, packet_data_length+8, auth_hash, 0, 8 );
		}
				
		DataInputStream is = new DataInputStream(new ByteArrayInputStream(input_buffer, 0, packet_data_length ));
		
		try{
			String	client_ip_address = request_dg.getAddress().getHostAddress();
			
			PRUDPPacketRequest	request = PRUDPPacketRequest.deserialiseRequest( null, is );
			
			Logger.log(new LogEvent(LOGID,
					"TRTrackerServerProcessorUDP: packet received: "
							+ request.getString())); 
				
			PRUDPPacket					reply 	= null;
			TRTrackerServerTorrentImpl	torrent	= null;
			
			if ( auth_user_bytes != null ){
				
				// user name is irrelevant as we only have one at the moment
	
				//<parg_home> so <new_packet> = <old_packet> + <user_padded_to_8_bytes> + <hash>
				//<parg_home> where <hash> = first 8 bytes of sha1(<old_packet> + <user_padded_to_8> + sha1(pass))
				//<XTF> Yes
				
								
				byte[] sha1_pw = null;
				
				if ( server.hasExternalAuthorisation()){
					
					try{
						URL	resource = new URL( "udp://" + server.getHost() + ":" + server.getPort() + "/" );
					
						sha1_pw = server.performExternalAuthorisation( resource, auth_user );
						
					}catch( MalformedURLException e ){
						
						Debug.printStackTrace( e );
						
					}
					
					if ( sha1_pw == null ){
				
						Logger.log(new LogEvent(LOGID, LogEvent.LT_ERROR,
								"TRTrackerServerProcessorUDP: auth fails for user '"
										+ auth_user + "'")); 

						reply = new PRUDPPacketReplyError( request.getTransactionId(), "Access Denied" );
					}
				}else{
					
					sha1_pw = server.getPassword();
				}
				
					// if we haven't already failed then check the PW
				
				if ( reply == null ){
					
					SHA1Hasher	hasher = new SHA1Hasher();
					
					hasher.update( input_buffer, 0, packet_data_length);
					hasher.update( auth_user_bytes );
					hasher.update( sha1_pw );
					
					byte[]	digest = hasher.getDigest();
					
					for (int i=0;i<auth_hash.length;i++){
						
						if ( auth_hash[i] != digest[i] ){
					
							Logger.log(new LogEvent(LOGID, LogEvent.LT_ERROR,
									"TRTrackerServerProcessorUDP: auth fails for user '"
											+ auth_user + "'")); 
	
							reply = new PRUDPPacketReplyError( request.getTransactionId(), "Access Denied" );
							
							break;
						}
					}
				}
			}
			
			int request_type = TRTrackerServerRequest.RT_UNKNOWN;
			
			if ( reply == null ){
				
				if ( server.isEnabled()){
					
					try{
						int	type = request.getAction();
						
						if ( type == PRUDPPacketTracker.ACT_REQUEST_CONNECT ){
							
							reply = handleConnect( client_ip_address, request );
							
						}else if (type == PRUDPPacketTracker.ACT_REQUEST_ANNOUNCE ){
							
							Object[] x = handleAnnounceAndScrape( client_ip_address, request, TRTrackerServerRequest.RT_ANNOUNCE );
							
							if ( x == null ){
								
								throw( new Exception( "Connection ID mismatch" ));
							}
							
							reply 	= (PRUDPPacket)x[0];
							torrent	= (TRTrackerServerTorrentImpl)x[1];
					
							request_type = TRTrackerServerRequest.RT_ANNOUNCE;
							
						}else if ( type == PRUDPPacketTracker.ACT_REQUEST_SCRAPE ){
							
							Object[] x = handleAnnounceAndScrape( client_ip_address, request, TRTrackerServerRequest.RT_SCRAPE );
							
							if ( x == null ){
								
								throw( new Exception( "Connection ID mismatch" ));
							}

							reply 	= (PRUDPPacket)x[0];
							torrent	= (TRTrackerServerTorrentImpl)x[1];
		
							request_type = TRTrackerServerRequest.RT_SCRAPE;
							
						}else{
							
							reply = new PRUDPPacketReplyError( request.getTransactionId(), "unsupported action");
						}
					}catch( Throwable e ){
						
						// e.printStackTrace();
						
						String	error = e.getMessage();
						
						if ( error == null ){
							
							error = e.toString();
						}
						
						reply = new PRUDPPacketReplyError( request.getTransactionId(), error );
					}
				}else{
										
					System.out.println( "UDP Tracker: replying 'disabled' to " + client_ip_address );
													
					reply = new PRUDPPacketReplyError( request.getTransactionId(), "UDP Tracker disabled" );
				}
			}
			
			if ( reply != null ){
				
				InetAddress address = request_dg.getAddress();
				
				ByteArrayOutputStream	baos = new ByteArrayOutputStream();
				
				DataOutputStream os = new DataOutputStream( baos );
										
				reply.serialise(os);
				
				byte[]	output_buffer = baos.toByteArray();
				
				DatagramPacket reply_packet = new DatagramPacket(output_buffer, output_buffer.length,address,request_dg.getPort());
							
				socket.send( reply_packet );
			
				server.updateStats( request_type, torrent, input_buffer.length, output_buffer.length );
			}
			
		}catch( Throwable e ){
			
			Logger.log(new LogEvent(LOGID,
					"TRTrackerServerProcessorUDP: processing fails", e)); 
		}finally{
			
			try{
				is.close();
				
			}catch( Throwable e ){
				
			}
		}
	}
	
	public void
	interruptTask()
	{
	}
	
	protected long
	allocateConnectionId(
		String	client_address )
	{
		try{
			random_mon.enter();
	
			long	id = random.nextLong();
			
			Long	new_key = new Long(id);
			
			connectionData	new_data = new connectionData( client_address, id );
			
				// check for timeouts
			
			if ( new_data.getTime() - last_timeout_check > 500 ){
				
				last_timeout_check = new_data.getTime();
				
				Iterator<Long>	it = connection_id_map.keySet().iterator();
							
				while(it.hasNext()){
					
					Long	key = it.next();
					
					connectionData	data = connection_id_map.get(key);
				
					if ( new_data.getTime() - data.getTime() > CONNECTION_ID_LIFETIME ){
						
						// System.out.println( "TRTrackerServerProcessorUDP: connection id timeout" );
						
						it.remove();
						
						List<connectionData> cds = connection_ip_map.get( client_address );

						if ( cds != null ){
							
							Iterator<connectionData> it2 = cds.iterator();
								
							while( it2.hasNext()){
								
								if ( it2.next().getID() == key ){
									
									it2.remove();
									
									break;
								}
							}
							
							if ( cds.size() == 0 ){
								
								connection_ip_map.remove( client_address );
							}
						}
						
					}else{
							// insertion order into map is time based - LinkedHashMap returns keys in same order
						
						break;
					}
				}
			}
				
			List<connectionData> cds = connection_ip_map.get( client_address );
			
			if ( cds == null ){
			
				cds = new ArrayList<connectionData>();
				
				connection_ip_map.put( client_address, cds );
			}
			
			cds.add( new_data );
			
			if ( cds.size() > 512 ){
				
				connectionData dead = cds.remove(0);
				
				connection_id_map.remove( dead.getID());
			}
			
			connection_id_map.put( new_key, new_data );
			
			// System.out.println( "TRTrackerServerProcessorUDP: allocated:" + id + ", connection id map size = " + connection_id_map.size());
			
			return( id );
			
		}finally{
			
			random_mon.exit();
		}
	}
	
	protected boolean
	checkConnectionId(
		String	client_address,
		long	id )
	{
		try{
			random_mon.enter();
			
			Long	key = new Long(id);
			
			connectionData data = (connectionData)connection_id_map.get( key );
			
			if ( data == null ){
				
				// System.out.println( "TRTrackerServerProcessorUDP: rejected:" + id + ", data not found" );
				
				return( false );
				
			}else{
				
				if ( SystemTime.getMonotonousTime() - data.getTime() > CONNECTION_ID_LIFETIME ){
					
					return( false );
				}
			}
			
			boolean	ok = data.getAddress().equals( client_address );
			
			// System.out.println( "TRTrackerServerProcessorUDP: tested:" + id + "/" + client_address + " -> " + ok );
			
			return( ok );
			
		}finally{
			
			random_mon.exit();
		}
	}
	
	protected PRUDPPacket
	handleConnect(
		String					client_ip_address,
		PRUDPPacketRequest		request )
	{
		long	conn_id = allocateConnectionId( client_ip_address );
		
		PRUDPPacket reply = new PRUDPPacketReplyConnect(request.getTransactionId(), conn_id );
		
		return( reply );
	}
	
		// returns reply packet and associated torrent if exists
	
	protected Object[]
	handleAnnounceAndScrape(
		String				client_ip_address,
		PRUDPPacketRequest	request,
		int					request_type )
	
		throws Exception
	{
		if ( !checkConnectionId( client_ip_address, request.getConnectionId())){
			
			return( null );
		}
				
		List		hashbytes	= new ArrayList();
		HashWrapper	peer_id		= null;
		int			port		= 0;
		String		event		= null;
		
		long		uploaded		= 0;
		long		downloaded		= 0;
		long		left			= 0;
		int			num_want		= -1;
		
		String		key				= null;
		
		if ( request_type == TRTrackerServerRequest.RT_ANNOUNCE ){
			
			if ( PRUDPPacketTracker.VERSION == 1 ){
				PRUDPPacketRequestAnnounce	announce = (PRUDPPacketRequestAnnounce)request;
				
				hashbytes.add(announce.getHash());
				
				peer_id		= new HashWrapper( announce.getPeerId());
				
				port		= announce.getPort();
				
				int	i_event = announce.getEvent();
				
				switch( i_event ){
					case PRUDPPacketRequestAnnounce.EV_STARTED:
					{
						event = "started";
						break;
					}
					case PRUDPPacketRequestAnnounce.EV_STOPPED:
					{
						event = "stopped";
						break;
					}
					case PRUDPPacketRequestAnnounce.EV_COMPLETED:
					{
						event = "completed";
						break;
					}					
				}
				
				uploaded 	= announce.getUploaded();
				
				downloaded	= announce.getDownloaded();
				
				left		= announce.getLeft();
				
				num_want	= announce.getNumWant();
				
				int	i_ip = announce.getIPAddress();
				
				if ( i_ip != 0 ){
					
					client_ip_address = PRHelpers.intToAddress( i_ip );
				}
			}else{
				
				PRUDPPacketRequestAnnounce2	announce = (PRUDPPacketRequestAnnounce2)request;
				
				hashbytes.add(announce.getHash());
				
				peer_id		= new HashWrapper( announce.getPeerId());
				
				port		= announce.getPort();
				
				int	i_event = announce.getEvent();
				
				switch( i_event ){
					case PRUDPPacketRequestAnnounce.EV_STARTED:
					{
						event = "started";
						break;
					}
					case PRUDPPacketRequestAnnounce.EV_STOPPED:
					{
						event = "stopped";
						break;
					}
					case PRUDPPacketRequestAnnounce.EV_COMPLETED:
					{
						event = "completed";
						break;
					}					
				}
				
				uploaded 	= announce.getUploaded();
				
				downloaded	= announce.getDownloaded();
				
				left		= announce.getLeft();
				
				num_want	= announce.getNumWant();
				
				int	i_ip = announce.getIPAddress();
				
				if ( i_ip != 0 ){
					
					client_ip_address = PRHelpers.intToAddress( i_ip );
				}
				
				key = "" + announce.getKey();
			}
		}else{
			
			PRUDPPacketRequestScrape	scrape = (PRUDPPacketRequestScrape)request;
			
			hashbytes.addAll(scrape.getHashes());
		}
		
		Map[]						root_out = new Map[1];
		TRTrackerServerPeerImpl[]	peer_out = new TRTrackerServerPeerImpl[1];
		
		TRTrackerServerTorrentImpl torrent =
			processTrackerRequest( 
				server, "", root_out, peer_out, 
				request_type,
				(byte[][])hashbytes.toArray(new byte[0][0]), null, null,
				peer_id, false, TRTrackerServerTorrentImpl.COMPACT_MODE_NONE, key, // currently no "no_peer_id" / "compact" in the packet and anyway they aren't returned / key
				event, false,
				port,
				0, 0,
				client_ip_address, client_ip_address,
				downloaded, uploaded, left,
				num_want,
				TRTrackerServerPeer.CRYPTO_NONE,
				(byte)1,
				0,
				null );
		
		Map	root = root_out[0];
		
		if ( request_type == TRTrackerServerRequest.RT_ANNOUNCE ){

			if ( PRUDPPacketTracker.VERSION == 1 ){
				PRUDPPacketReplyAnnounce reply = new PRUDPPacketReplyAnnounce(request.getTransactionId());
				
				reply.setInterval(((Long)root.get("interval")).intValue());
				
				List	peers = (List)root.get("peers");
				
				int[]	addresses 	= new int[peers.size()];
				short[]	ports		= new short[addresses.length];
				
				for (int i=0;i<addresses.length;i++){
					
					Map	peer = (Map)peers.get(i);
					
					addresses[i] 	= PRHelpers.addressToInt(new String((byte[])peer.get("ip")));
					
					ports[i]		= (short)((Long)peer.get("port")).shortValue();
				}
				
				reply.setPeers( addresses, ports );
				
				return( new Object[]{ reply, torrent });
				
			}else{
				
				PRUDPPacketReplyAnnounce2 reply = new PRUDPPacketReplyAnnounce2(request.getTransactionId());
				
				reply.setInterval(((Long)root.get("interval")).intValue());
				
				boolean	local_scrape = client_ip_address.equals( "127.0.0.1" );

				Map scrape_details = torrent.exportScrapeToMap( "", client_ip_address,!local_scrape );
				
				int	seeders 	= ((Long)scrape_details.get("complete")).intValue();
				int leechers 	= ((Long)scrape_details.get("incomplete")).intValue();

				reply.setLeechersSeeders(leechers,seeders);
				
				List	peers = (List)root.get("peers");
				
				int[]	addresses 	= new int[peers.size()];
				short[]	ports		= new short[addresses.length];
				
				for (int i=0;i<addresses.length;i++){
					
					Map	peer = (Map)peers.get(i);
					
					addresses[i] 	= PRHelpers.addressToInt(new String((byte[])peer.get("ip")));
					
					ports[i]		= (short)((Long)peer.get("port")).shortValue();
				}
				
				reply.setPeers( addresses, ports );
				
				return( new Object[]{ reply, torrent });
			}
			
		}else{
			
			if ( PRUDPPacketTracker.VERSION == 1 ){
				
				PRUDPPacketReplyScrape reply = new PRUDPPacketReplyScrape(request.getTransactionId());
				
				/*
				Long	interval = (Long)root.get("interval");
				
				if ( interval != null ){
					
					reply.setInterval(interval.intValue());
				}
				*/
				
				Map	files = (Map)root.get( "files" );
				
				byte[][]	hashes 			= new byte[files.size()][];
				int[]		s_complete		= new int[hashes.length];
				int[]		s_downloaded	= new int[hashes.length];
				int[]		s_incomplete	= new int[hashes.length];
				
				Iterator it = files.keySet().iterator();
				
				int	pos = 0;
				
				while(it.hasNext()){
					
					String	hash_str = (String)it.next();
					
					hashes[pos] = hash_str.getBytes( Constants.BYTE_ENCODING );
									
					Map	details = (Map)files.get( hash_str );
					
					s_complete[pos] 	= ((Long)details.get("complete")).intValue();
					s_incomplete[pos] 	= ((Long)details.get("incomplete")).intValue();
					s_downloaded[pos] 	= ((Long)details.get("downloaded")).intValue();
					
					pos++;
				}
				
				reply.setDetails( hashes, s_complete, s_downloaded, s_incomplete );
				
				return( new Object[]{ reply, torrent });
				
			}else{
				
				PRUDPPacketReplyScrape2 reply = new PRUDPPacketReplyScrape2(request.getTransactionId());
				
				/*
				Long	interval = (Long)root.get("interval");
				
				if ( interval != null ){
					
					reply.setInterval(interval.intValue());
				}
				*/
				
				Map	files = (Map)root.get( "files" );
				
				int[]		s_complete		= new int[files.size()];
				int[]		s_downloaded	= new int[s_complete.length];
				int[]		s_incomplete	= new int[s_complete.length];
				
				Iterator it = files.keySet().iterator();
				
				int	pos = 0;
				
				while(it.hasNext()){
					
					String	hash_str = (String)it.next();
														
					Map	details = (Map)files.get( hash_str );
					
					s_complete[pos] 	= ((Long)details.get("complete")).intValue();
					s_incomplete[pos] 	= ((Long)details.get("incomplete")).intValue();
					s_downloaded[pos] 	= ((Long)details.get("downloaded")).intValue();
					
					pos++;
				}
				
				reply.setDetails( s_complete, s_downloaded, s_incomplete );
				
				return( new Object[]{ reply, torrent });				
			}
		}
	}
	
	protected static class
	connectionData
	{
		private String		address;
		private long		id;
		private long		time;
		
		private
		connectionData(
			String		_address,
			long		_id )
		{
			address	= _address;
			id		= _id;
			
			time	= SystemTime.getMonotonousTime();
		}
		
		private String
		getAddress()
		{
			return( address );
		}
		
		private long
		getID()
		{
			return( id );
		}
		
		private long
		getTime()
		{
			return( time );
		}
	}
}
